package com.bizmda.bizsip.common;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.open.capacity.redis.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * @Author: 史正烨
 * @Date: 2022/8/21 17:57
 * @Description:
 */
@Slf4j
public class BizCircuitBreaker {
    private static RedisUtil redisUtil = null;
    public static final int CIRUIT_BREAKER_OPEN = 0;
    public static final int CIRUIT_BREAKER_HALF_OPEN = 1;
    public static final int CIRUIT_BREAKER_CLOSE = 2;
    public static final String PRE_REDIS_SUCCESS_KEY = "bizsip:sink:breaker:success:";
    public static final String PRE_REDIS_ERROR_KEY = "bizsip:sink:breaker:error:";
    public static final String REDIS_STATUS_KEY = "bizsip:sink:breaker:status";
    public static final String PRE_REDIS_HALFOPEN_KEY = "bizsip:sink:breaker:halfopen:";
    public static final String REDIS_SLEEP_WINDOW_IN_SECONDS_KEY = "bizsip:sink:breaker:sleep";

    /**
     * 判断将要调用的一系列Sink服务是否会触发熔断
     * @param args Sink服务ID（多个）
     * @throws BizException 如触发熔断，会抛出异常
     */
    public static void willCallSinks(String... args) throws BizException {
        log.debug("willCallSinks({})", args);
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        List<String> halfopenKeys = new ArrayList<>();
        for (String sinkId : args) {
            String halfopenKey = BizCircuitBreaker.PRE_REDIS_HALFOPEN_KEY + sinkId;
            Integer status = (Integer) redisUtil.hmGet(BizCircuitBreaker.REDIS_STATUS_KEY, sinkId);
            log.debug("Sink[{}]断路器状态:{}", sinkId, status);
            if (status == null || status == CIRUIT_BREAKER_CLOSE) {
                log.debug("Sink({})断路器为闭路状态,允许交易通过!", sinkId);
            } else if (status == CIRUIT_BREAKER_OPEN || status == CIRUIT_BREAKER_HALF_OPEN) {
                if (redisUtil.hasKey(halfopenKey)) {
                    throw new BizException(BizResultEnum.CIRCUIT_BREAKER_SINK_CLOSE,
                            "Sink(" + sinkId + ")断路器为开路/半开路状态,不允许交易通过!");
                } else {
                    log.info("Sink({})断路器为半开路状态,允许1个交易通过!", sinkId);
                    halfopenKeys.add(sinkId);
                }
            } else {
                throw new BizException((BizResultEnum.CIRCUIT_BREAKER_STATUS_UNKNOW), "Sink(" + sinkId + ")断路器状态异常:" + status);
            }
        }
        for (String sinkId : halfopenKeys) {
            String halfopenKey = BizCircuitBreaker.PRE_REDIS_HALFOPEN_KEY + sinkId;

            redisUtil.hmSet(BizCircuitBreaker.REDIS_STATUS_KEY, sinkId, CIRUIT_BREAKER_HALF_OPEN);
            long time = Convert.toLong(redisUtil.hmGet(BizCircuitBreaker.REDIS_SLEEP_WINDOW_IN_SECONDS_KEY, sinkId),5L);
            redisUtil.set(halfopenKey, 1, time);
            log.info("Sink[{}]断路器设置为半开路状态，下次允许尝试交易重试为[{}]秒以后", sinkId, time);
        }
    }

    /**
     * 设置Sink服务的熔断器状态
     * @param sinkId Sink服务ID
     * @param status 熔断器状态（0-开启，1-半开，2-关闭）
     */
    public static void setSinkStatus(String sinkId, int status) {
        log.debug("设置Sink({})断路器状态为[{}]", sinkId, status);
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        String statusKey = BizCircuitBreaker.REDIS_STATUS_KEY;
        redisUtil.hmSet(statusKey, sinkId, status);
    }

    /**
     * 获取Sink服务的熔断器状态
     * @param sinkId Sink服务ID
     * @return 熔断器状态（0-开启，1-半开，2-关闭）
     */
    public static Integer getSinkStatus(String sinkId) {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        String statusKey = BizCircuitBreaker.REDIS_STATUS_KEY + sinkId;
        return (Integer) redisUtil.hmGet(statusKey, sinkId);
    }

    public static Long getSuccessCount(String sinkId) {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        String successKey = BizCircuitBreaker.PRE_REDIS_SUCCESS_KEY + sinkId;
        Long a = (Long) redisUtil.get(successKey);
        if (a == null) {
            return 0L;
        }
        return a;
    }

    public static Long getErrorCount(String sinkId) {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        String errorKey = BizCircuitBreaker.PRE_REDIS_ERROR_KEY + sinkId;
        Long a = (Long) redisUtil.get(errorKey);
        if (a == null) {
            return 0L;
        }
        return a;
    }

    /**
     * 构建熔断器状态调试信息
     * @return 熔断器状态调试信息
     */
    public static String buildTrace() {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        String statusKey = BizCircuitBreaker.REDIS_STATUS_KEY;
        Set<Object> keySet = redisUtil.hKeys(statusKey);
        StringBuilder stringBuilder = new StringBuilder();
        for (Object key : keySet) {
            stringBuilder.append(buildTrace((String)key)).append("\n");
        }
        return stringBuilder.toString();
    }

    /**
     * 构建指定Sink服务的熔断器状态调试信息
     * @param sinkId 指定的Sink服务ID
     * @return 熔断器状态调试信息
     */
    public static String buildTrace(String sinkId) {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        String successKey = BizCircuitBreaker.PRE_REDIS_SUCCESS_KEY + sinkId;
        String errorKey = BizCircuitBreaker.PRE_REDIS_ERROR_KEY + sinkId;
        String halfopenKey = BizCircuitBreaker.PRE_REDIS_HALFOPEN_KEY + sinkId;

        long successCount = Convert.toLong(redisUtil.get(successKey),0L);
        long errorCount = Convert.toLong(redisUtil.get(errorKey),0L);
        long timeInSeconds = redisUtil.hasKey(successKey) ?
                redisUtil.getExpire(successKey) : 0L;
        long sleepWindowInSeconds = redisUtil.hasKey(halfopenKey) ?
                redisUtil.getExpire(halfopenKey) : 0L;
        Integer iStatus = Convert.toInt(redisUtil.hmGet(BizCircuitBreaker.REDIS_STATUS_KEY, sinkId),-1);
        String status;
        switch (iStatus) {
            case CIRUIT_BREAKER_OPEN:
                status = "开路";
                break;
            case CIRUIT_BREAKER_CLOSE:
                status = "闭路";
                break;
            case CIRUIT_BREAKER_HALF_OPEN:
                status = "半开";
                break;
            default:
                status = "未知";
        }

        long percent = 0;
        if ((successCount+errorCount) != 0) {
            percent = errorCount*100/(successCount + errorCount);
        }
        return StrUtil.format("Sink[{}]:{},(成功-{},失败-{},失败率-{}%),统计窗剩余-{}秒,半开剩余-{}秒",
                sinkId,status,successCount,errorCount,percent,timeInSeconds,sleepWindowInSeconds);
    }
}